using System;
using System.Collections;
using Token = Velocity4Net.Runtime.Parse.Token;
using Velocity4Net.Runtime.Visitor;
using System.IO;
using System.Text;
using Velocity4Net.Runtime.Parse.AST;
using Velocity4Net.Ctx;
using Velocity4Net.Runtime.Logging;
using Velocity4Net.Errors;

namespace Velocity4Net.Runtime.Directives
{
    /**
     *  VelocimacroProxy.java
     *
     *   a proxy Directive-derived object to fit with the current directive system
     *
     * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
     * @version $Id: VelocimacroProxy.java 898032 2010-01-11 19:51:03Z nbubna $
     */
    public class VelocimacroProxy : Directive
    {
        private String macroName;
        private String[] argArray = null;
        private String[] literalArgArray = null;
        private SimpleNode nodeTree = null;
        private int numMacroArgs = 0;
        private bool strictArguments;
        private bool localContextScope = false;
        private int maxCallDepth;
        private String bodyReference;

        /**
         * Return name of this Velocimacro.
         * @return The name of this Velocimacro.
         */
        public override String Name
        {
            get
            {
                return macroName;
            }
            set { macroName = value; }
        }

        /**
         * Velocimacros are always LINE type directives.
         * @return The type of this directive.
         */
        public override int Type
        {
            get
            {
                return 2;
            }
        }


        /**
         * sets the array of arguments specified in the macro definition
         * 
         * @param arr
         */
        public string []ArgArray
        {
            set
            {
                argArray = value;

                // for performance reasons we precache these strings - they are needed in
                // "render literal if null" functionality
                literalArgArray = new String[value.Length];
                for (int i = 0; i < value.Length; i++)
                {
                    literalArgArray[i] = ".literal.$" + argArray[i];
                }

                /*
                 * get the arg count from the arg array. remember that the arg array has the macro name as
                 * it's 0th element
                 */

                numMacroArgs = argArray.Length - 1;
            }
        }

        /**
         * @param tree
         */
        public SimpleNode NodeTree
        {
            set
            {
                nodeTree = value;
            }
        }

        /**
         * returns the number of ars needed for this VM
         * 
         * @return The number of ars needed for this VM
         */
        public int NumArgs
        {
            get
            {
                return numMacroArgs;
            }
        }

        public override bool Render(InternalContextAdapter context, TextWriter writer, INode node)
        {
            return render(context, writer, node, null);
        }

        /**
         * Renders the macro using the context.
         * 
         * @param context Current rendering context
         * @param writer Writer for output
         * @param node AST that calls the macro
         * @return True if the directive rendered successfully.
         * @throws IOException
         * @throws MethodInvocationException
         * @throws MacroOverflowException
         */
        public bool render(InternalContextAdapter context, TextWriter writer,
                              INode node, Renderable body)
    {
        // wrap the current context and add the macro arguments

        // the creation of this context is a major bottleneck (incl 2x HashMap)
         ProxyVMContext vmc = new ProxyVMContext(context, rsvc, localContextScope);

        int callArguments = node.jjtGetNumChildren();

        if (callArguments > 0)
        {
            // the 0th element is the macro name
            for (int i = 1; i < argArray.Length && i <= callArguments; i++)
            {
                /*
                 * literalArgArray[i] is needed for "render literal if null" functionality.
                 * The value is used in ASTReference render-method.
                 * 
                 * The idea is to avoid generating the literal until absolutely necessary.
                 * 
                 * This makes VMReferenceMungeVisitor obsolete and it would not work anyway 
                 * when the macro AST is shared
                 */
                vmc.addVMProxyArg(context, argArray[i], literalArgArray[i], node.jjtGetChild(i - 1));
            }
        }
        
        // if this macro was invoked by a call directive, we might have a body AST here. Put it into context.
        if( body != null )
        {
            vmc.addVMProxyArg(context, bodyReference, "", body);
        }

        /*
         * check that we aren't already at the max call depth
         */
        if (maxCallDepth > 0 && maxCallDepth == vmc.CurrentMacroCallDepth)
        {
            Object[] stack = vmc.MacroNameStack;

            StringBuilder _out = new StringBuilder(100)
                .Append("Max calling depth of ").Append(maxCallDepth)
                .Append(" was exceeded in macro '").Append(macroName)
                .Append("' with Call Stack:");
            for (int i = 0; i < stack.Length; i++)
            {
                if (i != 0)
                {
                    _out.Append("->");
                }
                _out.Append(stack[i]);
            }
            _out.Append(" at " + Log.formatFileString(this));
            rsvc.getLog().error(_out.ToString());
            
            // clean out the macro stack, since we just broke it
            while (vmc.CurrentMacroCallDepth > 0)
            {
                vmc.PopCurrentMacroName();
            }

            throw new MacroOverflowException(_out.ToString());
        }

        try
        {
            // render the velocity macro
            vmc.PushCurrentMacroName(macroName);
            nodeTree.Render(vmc, writer);
            vmc.PopCurrentMacroName();
            return true;
        }
        catch (ApplicationException e)
        {
            throw e;
        }
        catch (System.Exception e)
        {
            String msg = "VelocimacroProxy.render() : exception VM = #" + macroName + "()";
            rsvc.getLog().error(msg, e);
            throw new VelocityException(msg, e);
        }
    }

        /**
         * Initialize members of VelocimacroProxy.  called from MacroEntry
         */
        public void init(RuntimeServices rs)
        {
            rsvc = rs;

            // this is a very expensive call (ExtendedProperties is very slow)
            strictArguments = rs.Configuration.GetBoolean(
                RuntimeConstants.VM_ARGUMENTS_STRICT, false);

            // support for local context scope feature, where all references are local
            // we do not have to check this at every invocation of ProxyVMContext
            localContextScope = rsvc.GetBoolean(RuntimeConstants.VM_CONTEXT_LOCALSCOPE, false);
            if (localContextScope && rsvc.getLog().isWarnEnabled())
            {
                // only warn once per runtime, so this isn't obnoxious
                String key = "velocimacro.context.localscope.warning";
                Boolean alreadyWarned = (Boolean)rsvc.getApplicationAttribute(key);
                if (alreadyWarned == null)
                {
                    rsvc.SetApplicationAttribute(key, true);
                    rsvc.getLog()
                    .warn("The " + RuntimeConstants.VM_CONTEXT_LOCALSCOPE +
                          " feature is deprecated and will be removed in Velocity 2.0." +
                          " Instead, please use the $macro scope to store references" +
                          " that must be local to your macros (e.g. " +
                          "#set( $macro.foo = 'bar' ) and $macro.foo).  This $macro" +
                          " namespace is automatically created and destroyed for you at" +
                          " the beginning and end of the macro rendering.");
                }
            }

            // get the macro call depth limit
            maxCallDepth = rsvc.GetInt(RuntimeConstants.VM_MAX_DEPTH);

            // get name of the reference that refers to AST block passed to block macro call
            bodyReference = rsvc.GetString(RuntimeConstants.VM_BODY_REFERENCE, "bodyContent");
        }


        /**
         * Build an error message for not providing the correct number of arguments
         */
        private String buildErrorMsg(INode node, int numArgsProvided)
        {
            String msg = "VM #" + macroName + ": too "
              + ((NumArgs > numArgsProvided) ? "few" : "many") + " arguments to macro. Wanted "
              + NumArgs + " got " + numArgsProvided;
            return msg;
        }

        /**
         * check if we are calling this macro with the right number of arguments.  If 
         * we are not, and strictArguments is active, then throw TemplateInitException.
         * This method is called during macro render, so it must be thread safe.
         */
        public void checkArgs(InternalContextAdapter context, INode node, bool hasBody)
        {
            // check how many arguments we have
            int i = node.jjtGetNumChildren();

            // if macro call has a body (BlockMacro) then don't count the body as an argument
            if (hasBody)
                i--;

            // Throw exception for invalid number of arguments?
            if (NumArgs != i)
            {
                if (strictArguments)
                {
                    /**
                     * indicate col/line assuming it starts at 0 - this will be corrected one call up
                     */
                    throw new TemplateInitException(buildErrorMsg(node, i),
                        context.CurrentTemplateName, 0, 0);
                }
                else if (rsvc.getLog().isDebugEnabled())
                {
                    rsvc.getLog().debug(buildErrorMsg(node, i));
                    return;
                }
            }
        }
    }

}
